package oauth2

import (
	"errors"
	"fmt"
	"gitee.com/yysf_xin/go-common/tools"
	"gitee.com/yysf_xin/go-common/tools/template"
	"github.com/valyala/fastjson"
	"golang.org/x/oauth2"
	"io"
	"mime"
	"net/http"
	"net/url"
	"os"
	"strconv"
	"strings"
	"time"
)

type Config struct {
	TokenURL   string
	Body       string
	AuthType   int
	HeaderName string
}

type Client struct {
	cfg    *Config
	fn     template.StringMapping
	parent http.RoundTripper
	source oauth2.TokenSource
}

func New(cfg *Config, fn template.StringMapping, parent http.RoundTripper) *Client {
	if parent == nil {
		parent = http.DefaultTransport
	}
	c := &Client{cfg, fn, parent, nil}
	c.source = oauth2.ReuseTokenSource(nil, c)
	return c
}

func (t *Client) RoundTrip(r *http.Request) (*http.Response, error) {
	token, err := t.source.Token()
	if err != nil {
		return nil, err
	}
	if t.cfg.AuthType == 0 {
		r.Header.Set("Authorization", token.Type()+" "+token.AccessToken)
	} else if t.cfg.AuthType == 1 {
		//q := r.URL.Query()
		//		//q.Add("access_token", token.AccessToken)
		//		//r.URL.RawQuery = q.Encode()
		r.URL.RawQuery += "&access_token=" + token.AccessToken
	} else if t.cfg.HeaderName != "" {
		r.Header.Set(t.cfg.HeaderName, token.AccessToken)
	}
	return t.parent.RoundTrip(r)
}

func (t *Client) Token() (token *oauth2.Token, err error) {
	if strings.Contains(t.cfg.TokenURL, "${") {
		t.cfg.TokenURL = os.Expand(t.cfg.TokenURL, t.fn)
	}
	if strings.Contains(t.cfg.Body, "${") {
		t.cfg.Body = os.Expand(t.cfg.Body, t.fn)
	}
	method := "GET"
	if t.cfg.Body != "" {
		method = "POST"
	}
	req, err := http.NewRequest(method, t.cfg.TokenURL, strings.NewReader(t.cfg.Body))

	if err != nil {
		return
	}
	if strings.Contains(t.cfg.Body, "&") {
		req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
	} else {
		req.Header.Set("Content-Type", "application/json")
	}

	resp, err := t.parent.RoundTrip(req)

	if resp != nil && resp.Body != nil {
		defer resp.Body.Close()
	}

	if err != nil {
		return
	}
	body, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
	if err != nil {
		return nil, fmt.Errorf("oauth2: cannot fetch token: %v", err)
	}
	if code := resp.StatusCode; code < 200 || code > 299 {
		return nil, &oauth2.RetrieveError{
			Response: resp,
			Body:     body,
		}
	}

	content, _, _ := mime.ParseMediaType(resp.Header.Get("Content-Type"))
	switch content {
	case "application/x-www-form-urlencoded", "text/plain":
		vals, err := url.ParseQuery(string(body))
		if err != nil {
			return nil, err
		}
		token = &oauth2.Token{
			AccessToken:  vals.Get("access_token"),
			TokenType:    vals.Get("token_type"),
			RefreshToken: vals.Get("refresh_token"),
		}
		e := vals.Get("expires_in")
		expires, _ := strconv.Atoi(e)
		if expires != 0 {
			token.Expiry = time.Now().Add(time.Duration(expires) * time.Second)
		}
	default:
		var root *fastjson.Value
		root, err = fastjson.ParseBytes(body)
		if err != nil {
			return
		}
		expireIn := findIntVal(root, "expires_in", "expireIn", "expire_in")
		var expiry time.Time
		if expireIn > (1 << 30) {
			expiry = time.UnixMilli(expireIn)
		} else {
			expiry = time.Now().Add(time.Duration(expireIn) * time.Second)
		}

		token = &oauth2.Token{
			AccessToken:  findStrVal(root, "accessToken", "access_token"),
			TokenType:    findStrVal(root, "token_type", "tokenType"),
			RefreshToken: findStrVal(root, "refreshToken", "refresh_token"),
			Expiry:       expiry,
		}
	}
	if token.AccessToken == "" {
		tools.Log.Println(tools.BytesToString(body))
		return nil, errors.New("oauth2: server response missing access_token")
	}
	return token, nil

}

func findStrVal(t *fastjson.Value, keys ...string) string {
	for _, v := range keys {
		val := t.GetStringBytes(v)
		if val != nil {
			return string(val)
		}
	}
	return ""
}
func findIntVal(t *fastjson.Value, keys ...string) int64 {
	for _, v := range keys {
		val := t.GetInt64(v)
		if val != 0 {
			return val
		}
	}
	return 0
}
